#betrayed

#import bisect
import re
#import select
import socket
from socket import gethostname, AF_INET6, SOCK_STREAM, error as sock_error
#import string
import sys
import time
#import types
import threading
from collections import deque
import select

import logging

VERSION = 0,0,1
DEBUG = 1

class Sender(): 
    source = re.compile(r'([^!]*)!?([^@]*)@?(.*)')
    def __init__(self, nope,source, args): 
        match = Sender.source.match(source or '')
        self.nick, self.user, self.host = match.groups()
        if len(args) > 1: 
            target = args[1]
        else: target = None

_linesep = re.compile(r"\r?\n")

class IRCCASEMAPPING:
    RFC1459         = 0
    RFC1459_STRICT  = 1
    ASCII           = 2

class Protocol(threading.Thread):
    def connect (self, ircserver, port, nicknames, password=None,  username=None,  ircname=None,  locala="",  localp=0, version="crappy irc", 
        debug_log_file="log/debug.log", debug = False, source_addr = None):
        #need to check for nick in use error and provide secondary nick or possible random nick 
        if self.connected:
            self.disconnect("Rotate")
        self.data = ""
        self.buffer = ""
        self.server = ircserver
        self.port = port
        self.password = password
        self.username = username
        self.ircname = ircname
        self.localhost = gethostname()
        self.locala = locala
        self.localp = localp
        #self.socket = socket(AF_INET6, SOCK_STREAM)
        self.source_address = source_addr
        self.alive = True
        self.version = version
        self.support = {}
        self.user_flags = "ov"
        self.user_prefixes = "@+"
        self.case_mapping = IRCCASEMAPPING.RFC1459
        self.shutting_down = False
        self.registered = False
        self.set_flags = "beIkl"
        self.unset_flags = "beIk"

        self.sendqueue = deque()
        self.sendqueue_last = 0
        self.sendqueue_wait = 0

        # Start queuing once our time >= current time + sendqueue_delay
        self.sendqueue_delay = 10
        # Penalty for each message sent in seconds
        self.sendqueue_penalty = 2

        # How often to PING server when we haven't received data
        self.timeout_check = 90
        # Assume connection is dead after timeout_wait
        self.timeout_wait = 30
        self.last_recv = time.time()
        self.last_ping = 0
        self.check_timeout = False

        self.logger = logging.getLogger('cib')
        self.logger.setLevel(logging.INFO)

        ch = logging.StreamHandler(sys.stdout)
        ch.setLevel(logging.INFO)
        self.logger.addHandler(ch)

        ch = logging.StreamHandler(sys.stderr)
        ch.setLevel(logging.ERROR)
        self.logger.addHandler(ch)
        
        self._stop = threading.Event()
        
        self.debug_log = None
        self.debug_log_file = debug_log_file
        if debug:
            fh = logging.FileHandler(self.debug_log_file)
            fh.setLevel(logging.DEBUG)
            self.logger.addHandler(fh)

            self.debug_toggle()

        try:
            #self.socket.connect((self.server, self.port))
            self.socket = socket.create_connection((self.server, self.port), socket.getdefaulttimeout(), self.source_address if self.source_address else None)
        except:
            #self.socket.close()
            self.socket = None
            self.logger.info("!! Could not connect to host")
            self.alive = False
            return False
        self.connected = 1
        if self.password:
            self.pass_(self.password)
        
        self.nicknames = nicknames[:]

        self.send("CAP REQ :multi-prefix")
        self.send("CAP END")

        self.nick(self.nicknames.pop(0))
        self.user(self.username, self.ircname)
        return self
        
    def __init__(self):
        threading.Thread.__init__(self)
        self.connected = 0
        self.socket = None
        self.alive = False
        
    def debug_toggle(self):
        if self.debug_log:
            #self.debug_log.close()
            self.logger.setLevel(logging.INFO)
            self.debug_log = None
        else:
            try:
                #self.debug_log = open(self.debug_log_file, "a+", 0)
                self.logger.setLevel(logging.DEBUG)
            except:
                self.logger.info("!@! Error opening debug log")
                return False
        return True      

    def send(self, message):
        """Internal function to send data through the socket.

        Parameters:
        message -- Raw IRC message
        """
        if not self.socket:
            return
        curtime = time.time()
        if self.sendqueue_wait < curtime:
            self.sendqueue_wait = curtime

        if self.sendqueue or round(self.sendqueue_wait - curtime, 2) >= self.sendqueue_delay:
            self.sendqueue.append(message)
        else:
            self.send_immediate(message)

    def send_immediate(self, message):
        """Sends data to server bypassing send queue.

        Parameters:
        message -- Raw IRC message
        """
        curtime = time.time()
        if self.sendqueue_wait < curtime:
            self.sendqueue_wait = curtime

        self.send_data(message)
        self.sendqueue_wait += self.sendqueue_penalty

    def send_data(self, message):
        """Internal function to send data to server

        Parameters:
        message -- Raw IRC message
        """
        if self.socket is None:
            self.disconnect("Connection reset by server")
        try:
            totalsent = 0
            data = message + "\r\n"
            msglen = len(data)
            while totalsent < msglen:
                sent = self.socket.send(data[totalsent:])
                if sent == 0:
                    # Broken socket
                    self.socket = None
                    self.disconnect("Broken socket")
                    return 0
                totalsent = totalsent + sent
            self.sendqueue_last = time.time()
            if self.debug_log:
                self.logger.debug("FROM LOCAL %s" % (message))
                #print >>self.debug_log, ("FROM LOCAL %s" % (message))
        except sock_error, x:
            self.disconnect("Connection reset by server")
        except AttributeError:
           # Wth? We just checked if socket is none.
           self.socket = None
           self.disconnect("Connection reset by server 2")

    def process_sendqueue(self):
        """Internal function for processing the send buffer, to prevent flooding.
        
        Parameters:
        None
        """
        if not self.sendqueue:
            return
        curtime = time.time()

        if (curtime - self.sendqueue_last) >= self.sendqueue_penalty:
            while 1:
                msg = self.sendqueue.popleft()
                self.send_data(msg)
                self.sendqueue_wait += self.sendqueue_penalty
                if not self.sendqueue:
                    break
                if round(self.sendqueue_wait - curtime, 2) >= self.sendqueue_delay:
                    break

    def pass_(self, password):
        """Send a 'PASS' message and password when connecting.

        Parameters:
        password
        """
        self.send("PASS %s" % password)
        
    def user(self, realname, username):
        """Send a 'USER' message when connecting

        Parameters:
        realname -- Real Name
        username -- User Name
        """
        self.send("USER %s 0 * :%s" % (username, realname))
        
    def nick(self, nickname):
        """Change/Set IRC Nickname

        Parameters:
        nickname -- Nick to change to
        """
        #self.nickname = nickname
        self.send("NICK %s" % nickname)
        
    def nextnick(self):
        if len(self.nicknames) > 0:
            self.nick(self.nicknames.pop(0))
        else:
            self.disconnect("")
            
    def quit(self, message):
        """Send QUIT message. Use the disconnect() method instead.

        Parameters:
        message -- Optional quit message
        """
        self.send("QUIT%s" % (message and (" :%s" % message)))
        
    def disconnect(self, message):
        """Close socket and send QUIT.

        Parameters:
        message -- Quit message
        """
        if not self.connected:
            return
        self.connected = 0
        self.quit(message)
        try:
            self.socket.close()
        except sock_error,  x:
            pass
        self.socket=None
        self.alive = False

    def terminate(self, message):
        """Disconnect and stop reconnects

        Parameters:
        message -- Quit message
        """
        self.shutting_down = True
        self.disconnect(message)

    def recv(self):
        try:
		    data=self.socket.recv(2**14)
        except:
            self.disconnect("Server reset by 1")
            return
        if not data:
            self.disconnect("Server reset by 2")
            return
        lines = _linesep.split(self.data+data)
        #print lines
        self.data = lines[-1]
        lines = lines[:-1]
        self.last_recv = time.time()
        for line in lines:
            if self.debug_log:
                #print >>self.debug_log, "FROM SERVER: %s" % line
                self.logger.debug("FROM SERVER: %s" % line)
            if not line: 
                continue
            self.process_data(line)
        return line

    def process_data(self,data):
        line = data

        if line is None:
            return
        if line.startswith(':'): 
            source, line = line[1:].split(' ', 1)
        else: source = None
        if ' :' in line: 
            argstr, text = line.split(' :', 1)
        else: argstr, text = line, ''
        args = argstr.split()
        sender = Sender(self, source, args)
        if args[0] == "PING": 
            self.send_immediate("PONG %s" % text)
        if args[0] == "PONG":
            self.check_timeout = False
        #print tuple([text] + args)
        self.dispatch(sender, tuple([text] + args))

    def dispatch(self, sender, args):
        event = args[1]
        if not self.registered and (event == "433" or event == "437"):
            # 437 is ERR_UNAVAILRESOURCE on EFNet at least
            # We don't want to cycle nicks if we're already registered
            self.nextnick()
        elif event == "001":
            self.registered = True
            self.logger.debug("!!! 001 handled -- connection authorized")
            self.nickname = args[2]

            channels = []
            keys = []
            for ch in self.channels:
                try:
                    """channels with keys need to come first"""
                    chan, key = ch.split(':')
                    channels.insert(0, chan)
                    keys.insert(0, key)
                except:
                    chan = ch
                    channels.append(chan)

            self.joinc(",".join(channels), ",".join(keys))
        elif event == "005":
            """If we follow the protocol, then we are getting notice that we
            need to connect to a different server. If we follow convention, we
            are getting server info."""
            self.parse005(args)

        elif event == "NICK":
            if sender.nick == self.nickname:
                self.nickname = args[0]
        
        elif event == "PRIVMSG":
            """XXX: This doesn't belong here however the logging plugin will show the events 
            out of order otherwise, we need a better system to queue event handler priority
            in the future."""
            if len(args[0]) > 1 and args[0][0] == '\001' and args[0][-1] == '\001':
                ctcp_cmd = args[0].strip('\001')
                if len(ctcp_cmd) > 1:
                    if bool(re.match('VERSION', ctcp_cmd, re.I)):
                        self.ctcp_reply(sender.nick, "VERSION %s" % self.version)
                    elif bool(re.match('PING', ctcp_cmd, re.I)):
                        self.ctcp_reply(sender.nick, "%s" % ctcp_cmd)
                    elif bool(re.match('TIME', ctcp_cmd, re.I)):
                        self.ctcp_reply(sender.nick, "TIME %s" % 
                            (time.strftime("%a %b %d %H:%M:%S %Y", time.localtime())))
                
    def get_args(self,data):
        return data.split()[4:]
        
    def get_args1(self,data):
        return data.split()[3:]
        
    def get_username(self,data):
        return data.split("!")[0].strip(":")
        
    def get_hostname(self,data):
        return data.split("!")[1].split(" ")[0]
        
    def run(self):
        while self.alive:
            if not self.sendqueue:
                (rlist, wlist, exlist) = select.select([self.socket], [], [], 1)
            else:
                (rlist, wlist, exlist) = select.select([self.socket], [], [], 0.5)
            if self._stop.is_set():
                self.logger.info("!!! Terminating due to SIGTERM")
                self.terminate("SIGTERM received, exiting")
                break
            if rlist:
                data = self.recv()
            if self.check_timeout and time.time() - self.last_ping >= self.timeout_wait:
                # Probably lost connection - give up
                self.disconnect("Lost connection")
                break
            if not self.check_timeout and time.time() - self.last_recv >= self.timeout_check:
                self.check_timeout = True
                self.last_ping = time.time()
                self.send_immediate("PING :TIMEOUTCHECK")
            self.process_sendqueue()
            
        if self.debug_log:
            self.debug_toggle()
            #self.process_data(data)

    def is_channel(self, string):
        if 'chantypes' in self.support:
            return string and string[0] in self.support['chantypes']
        else:
            return string and string[0] in "#&"

    def parse005(self, args):
        data = args[3:]

        for item in data:
            if "=" in item:
                key, values = item.split("=")
                self.support[key.lower()] = values
            else:
                self.support[item.lower()] = True
                key = item
            
            if key == "PREFIX":
                prefix = self.support['prefix']
                if prefix != True:
                    m = re.match('\((\w+)\)(.+)', prefix)
                    modes, prefixes = m.groups()
                    if len(modes) == len(prefixes):
                        self.user_flags = modes
                        self.user_prefixes = prefixes
            elif key == "CASEMAPPING":
                cmptype = self.support['casemapping']
                if cmptype != True:
                    if cmptype == 'rfc1459':
                        self.case_mapping = IRCCASEMAPPING.RFC1459
                    elif cmptype == 'strict-rfc1459':
                        self.case_mapping = IRCCASEMAPPING.RFC1459_STRICT
                    elif cmptype == 'ascii':
                        self.case_mapping = IRCCASEMAPPING.ASCII
            elif key == "CHANMODES":
                try:
                    (a, b, c, d) = self.support['chanmodes'].split(',')
                    self.set_flags = a + b + c
                    self.unset_flags = a + b
                except ValueError:
                    pass

    """Yar, here be output methods"""
    
    def joinc(self, channel,key=""):
        """Join a channel.

        Parameters:
        channel -- Channel name
        key -- Channel key
        """

        #channel and key match string set up as "channel, channel1" and "channel,channel1"
        channels = channel.replace(" ","")
        keys = key.replace(" ","")
        self.send("JOIN %s%s" % (channels, keys and (" " + keys) or ""))
        
    def privmsg(self, who, what):
        """Send a message.

        Parameters:
        who -- Channel or nick
        what -- Message
        """

        self.send("PRIVMSG %s :%s" % (who, what))
        
    def kick(self, channel, nick, reason="kicked"):
        """Kick someone from a channel.

        Parameters:
        channel -- Channel from which to kick someone
        nick -- User to kick
        reason -- Reason for kick. Optional (default: "kicked")
        """

        self.send("KICK %s %s :%s" % (channel, nick, reason))
        
    def action(self, target, action):
        """Send 'ACTION' to channel or user. AKA, /me

        Parameters:
        target -- User or channel
        action -- Text to send
        """
        self.ctcp("ACTION", target, action)
        
    def ctcp(self, ctcptype, target, parameter=""):
        """Send a CTCP message.

        Parameters:
        ctcptype -- Which CTCP command to send
        target -- User or channel
        parameter -- Data for CTCP command
        """

        ctcptype = ctcptype.upper()
        self.privmsg(target, "\001%s%s\001" % (ctcptype, parameter and (" " + parameter) or ""))
        
    def notice(self, target, text):
        """Send a NOTICE message.

        Parameters:
        target -- Channel or User
        text -- Message
        """
        self.send("NOTICE %s :%s" % (target, text))
        
    def whois(self, target):
        """Send a WHOIS request

        Parameters:
        target -- User to lookup
        """
        self.send("WHOIS ,%s" % (target))
        
    def mode(self, target, mode, params=None):
        """Set a user or channel mode.

        Parameters:
        target -- Channel or user
        mode -- Mode string
        params -- Positional parameters for mode string
        """
        self.send("MODE %s %s %s" % (target, mode, params))
        
    def usermode(self, target, mode):
        self.send("MODE %s %s" % (target, mode))
        
    def ctcp_reply(self, who, what):
        """Send a reply to CTCP Request.

        Parameters:
        who -- User
        what -- CTCP Response
        """
        self.notice(who, "\001%s\001" % what)
    def topic(self, channel, topic=None):
        """Get/Set channel topic.

        Parameters:
        channel -- Channel in which to set topic
        topic -- Topic to set. Optional (default: None)
        """
        self.send("TOPIC %s%s" % (channel, topic  and (" :%s" % topic)))
        
    def operator(self, user, password):
        self.send("OPER %s %s" % (user, password))
        
    def names(self,channel=None):
        """Request name-list.

        Parameters:
        channel -- List of channels from which to request names.
        """
        #expects a list of channels ["#channel1", "channel2"]
        self.send("NAMES%s" % (channel and( " " + ",".join(channel) or "")))
        
    def list(self, channel=None, target=""):
        """Request channel list.

        Parameters:
        channel -- List of wildcard queries
        target -- ??
        """
        #expects a list of channel
        self.send("LIST%s%s" % (channel and (" " + ",".join(channel)) or "", target and(" " + target)))
        
    def part(self,channel,message=None):
        """Leave a channel.

        Parameters:
        channel -- Channel to leave
        message -- Parting message. Optional
        """
        channels = channel.replace(" ","")
        self.send("PART %s%s" % (channels, message and (" :%s" % message)))

    def modes(self, data):
        count = 0
        type = ""
        rdata = []
        dsplit = data.split()
        try:
            bdata, edata = dsplit[0], dsplit[1:]
        except:
            return None
        if bdata[0] not in "+-": return []
        for i in bdata:
            if i == '-':
                mflags = self.unset_flags + self.user_flags
                type = '-'
            elif i == '+':
                mflags = self.set_flags + self.user_flags
                type = '+'
            elif i in mflags:
                if len(edata) >= count + 1:
                    rdata.append ([type, i, edata[count]])
                    count = count +1
                else: 
                    rdata.append([type, i, None])
            else:
                rdata.append([type, i, None])
        return rdata

    def irccmp(self, str1, str2):
        """Compare two strings case-independently using the IRC servers CASEMAPPING setting

        Parameters:
        str1, str2 -- Strings to compare
        """
        s1 = self.irclower(str1)
        s2 = self.irclower(str2)
        # Yes I know this sucks.
        return cmp(s1, s2)
        
    def irclower(self, text):
        """Convert a string to lower-case using the IRC Servers CASEMAPPING setting

        Parameters:
        text -- String to convert
        """
        start = ord('A')
        text_lower = ""
        
        if self.case_mapping == IRCCASEMAPPING.RFC1459_STRICT:
            end = ord(']')
        elif self.case_mapping == IRCCASEMAPPING.ASCII:
            end = ord('Z')
        else:
            end = ord('^')
        
        for c in text:
            if ord(c) >= start and ord(c) <= end:
                text_lower += chr(ord(c) + 32)
            else:
                text_lower += c
        return text_lower

    def prefix_to_mode(self, char):
        """Checks of a character is a prefix (in a NAMES reply)

        Parameters:
        char - Character to check

        Returns the mode character (i.e. 'o' for '@')
        """
        loc = self.user_prefixes.find(char)

        if loc != -1:
            return self.user_flags[loc]
        else:
            return None
    
    def mode_to_prefix(self, char):
        """Checks of a character is a mode that sets a flag on a user in a channel

        Parameters:
        char - Character to check

        Returns the prefix character (i.e. '@' for 'o')
        """
        loc = self.user_flags.find(char)

        if loc != -1:
            return self.user_prefixes[loc]
        else:
            return None
